package imagingbook.sift;

import ij.IJ;
import imagingbook.lib.util.Enums;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class SiftMatcher {
	
	// The main purpose of this enumeration class is to associate
	// the distance norm type with the corresponding ArrayMatcher.
	public enum FeatureDistanceNorm {
		L1 (new L1Matcher()), 	// matchers are defined below
		L2 (new L2Matcher()), 
		Linf (new LinfMatcher());
		
		ArrayMatcher matcher;						// object variable
		FeatureDistanceNorm(ArrayMatcher matcher) { // constructor
			this.matcher = matcher;
		}
		
		public static final String[] names = Enums.getEnumNames(FeatureDistanceNorm.class);
	}
	
	public static class Parameters {
		public FeatureDistanceNorm norm = FeatureDistanceNorm.L2;
		public double rmMax = 0.8;	// = \bar{\rho}_{match}
		public boolean sort = true;
	}
	
	Parameters params;
	final SiftDescriptor[] fA;

	// constructor - using all default parameters
	public SiftMatcher(List<SiftDescriptor> sfA) {
		this.fA = sfA.toArray(new SiftDescriptor[0]);
		params = new Parameters();
	}
	
	// constructor - using specific parameters
	public SiftMatcher(List<SiftDescriptor> sfA, Parameters params) {
		this.fA = sfA.toArray(new SiftDescriptor[0]);
		this.params = params;
	}
	
	public List<SiftMatch> matchDescriptors(List<SiftDescriptor> sfB) {
		SiftDescriptor[] fB = sfB.toArray(new SiftDescriptor[0]);
		List<SiftMatch> matches = new ArrayList<SiftMatch>(fA.length);
				
		for (int i = 0; i < fA.length; i++) {
			SiftDescriptor si = fA[i];
			int i1 = -1;
			int i2 = -1;
			double d1 = Double.MAX_VALUE;
			double d2 = Double.MAX_VALUE;
			
			for (int j = 0; j < fB.length; j++) {
				double d = dist(si, fB[j]);
				if (d < d1) {	// new absolute minimum distance
					i2 = i1;	// old best becomes second-best
					d2 = d1;
					i1 = j;
					d1 = d;
				}
				else // not a new absolute min., but possible second-best
					if (d < d2) { // new second-best
						i2 = j;
						d2 = d;
					}
			}
			if (i2 >= 0 && d2 > 0.001 && d1/d2 < params.rmMax) {
				SiftDescriptor s1 = fB[i1];
				SiftMatch m = new SiftMatch(si, s1, d1);
				matches.add(m);
			}
		}
		if (params.sort) Collections.sort(matches);  // sort matches to ascending distance d1
		return matches;
	}
	
	double dist(SiftDescriptor d1, SiftDescriptor d2) {
		final ArrayMatcher matcher = params.norm.matcher;
		return matcher.getDistance(d1.features, d2.features);
	}
	
	// array matcher classes:
	
	static abstract class ArrayMatcher {
		abstract double getDistance(int[] f1, int[] f2);
		abstract void check();
	}
	
	static class L1Matcher extends ArrayMatcher {
		
		void check() {
			IJ.log("this is a L1 matcher");
		}
		
		double getDistance(final int[] f1, final int[] f2) {
			int sum = 0;
			for (int i=0; i<f1.length; i++) {
				sum = sum + Math.abs(f1[i] - f2[i]);
			}
			return sum;
		}
	}
	
	static class L2Matcher extends ArrayMatcher {
		
		void check() {
			IJ.log("this is a L2 matcher");
		}
		
		double getDistance(final int[] f1, final int[] f2) {
			int sum = 0;
			for (int i=0; i<f1.length; i++) {
				int d = f1[i] - f2[i];
				sum = sum + d * d;
			}
			return Math.sqrt(sum);
		}
	}
	
	static class LinfMatcher extends ArrayMatcher {
		
		void check() {
			IJ.log("this is a Linf matcher");
		}
		
		double getDistance(final int[] f1, final int[] f2) {
			int dmax = 0;
			for (int i=0; i<f1.length; i++) {
				int d = Math.abs(f1[i] - f2[i]);
				dmax = Math.max(dmax, d);
			}
			return dmax;
		}
	}

}
